﻿using System;
using System.Collections;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;

namespace Renci.Data.Interop.Serialization
{
    public partial class InteropSerializer
    {
        private static void HandleWriteNamespaceDeclarations(System.Xml.XmlWriter writer, object obj)
        {
            var nameSpacesProperty = (from prop in obj.GetType().GetProperties()
                                      where
                                         prop.PropertyType == typeof(XmlSerializerNamespaces)
                                         && prop.GetCustomAttributes(typeof(XmlNamespaceDeclarationsAttribute), true).SingleOrDefault() != null
                                      select prop).SingleOrDefault();
            if (nameSpacesProperty != null)
            {
                XmlSerializerNamespaces nameSpaces = nameSpacesProperty.GetValue(obj, null) as XmlSerializerNamespaces;
                foreach (XmlQualifiedName qn in nameSpaces.ToArray())
                {
                    var prefix = writer.LookupPrefix(qn.Namespace);
                    if (string.IsNullOrEmpty(prefix))
                    {
                        writer.WriteAttributeString("xmlns", qn.Name, null, qn.Namespace);
                    }
                    else if (!prefix.Equals(qn.Name, StringComparison.OrdinalIgnoreCase))
                    {
                        writer.WriteAttributeString("xmlns", qn.Name, null, qn.Namespace);
                    }
                }
            }
        }

        private static void HandleWriteAttributes(System.Xml.XmlWriter writer, object obj)
        {
            var propertiesToAttributesMap = GetPropertiesAttributeMap(obj.GetType());
            
            var properties = from prop in propertiesToAttributesMap.OfType<PropertyAttributeSerializationInfo>()
                             where prop.IsAttribute
                             select prop;

            //  TODO:   Put into class so dont have to resolve it each time ???
            var currentTypeAttribute = obj.GetType().GetCustomAttributes(true).OfType<XmlTypeAttribute>().SingleOrDefault();

            //  Handle all properties tha serialized as attributes first
            foreach (var property in properties)
            {
                var propertyValue = property.GetPropertyValue(obj);

                if (propertyValue == null)
                {
                    //  TODO:   check what need to do for properties with Nullable=true
                    continue;
                }

                if (property.HasDefaultValue(propertyValue))
                    continue;

                var propertyAttribute = (from attr in
                                             (from a in propertiesToAttributesMap
                                              where a.Property == property.Property
                                              select a).OfType<PropertyAttributeSerializationInfo>()
                                         where
                                         attr.IsAttribute
                                         && attr.Type == propertyValue.GetType()
                                         select attr).SingleOrDefault();


                if (propertyAttribute != null)
                {

                    switch (propertyAttribute.Form)
                    {
                        case System.Xml.Schema.XmlSchemaForm.None:
                        case System.Xml.Schema.XmlSchemaForm.Unqualified:
                            {
                                if (propertyAttribute.Name.Namespace == currentTypeAttribute.Namespace)
                                {
                                    writer.WriteStartAttribute(propertyAttribute.Name.Name, propertyValue.ToString());
                                }
                                else
                                {
                                    writer.WriteStartAttribute(propertyAttribute.Name.Name, propertyAttribute.Name.Namespace);
                                }
                                break;
                            }
                        case System.Xml.Schema.XmlSchemaForm.Qualified:
                            {
                                writer.WriteStartAttribute(propertyAttribute.Name.Name, propertyAttribute.Name.Namespace);
                                break;
                            }
                        default:
                            throw new InvalidOperationException();
                    }

                    WriteValue(writer, propertyValue);

                    writer.WriteEndAttribute();
                }
            }
        }

        private static void HandleWriteNodeElements(System.Xml.XmlWriter writer, object obj)
        {
            var propertiesToAttributesMap = GetPropertiesAttributeMap(obj.GetType());

            var properties = from prop in propertiesToAttributesMap
                             where 
                                prop.HasElements
                             select prop;

            //  Handle all properties that serialized as element
            foreach (var property in properties)
            {
                var propertyValue = property.GetPropertyValue(obj);

                if (propertyValue == null)
                {
                    //  TODO:   check what need to do for properties with Nullable=true
                    continue;
                }

                if (property.HasDefaultValue(propertyValue))
                {
                    continue;
                }

                if (
                    propertyValue is string
                    || propertyValue is XmlNode)
                {
                    WriteComplexValue(writer, obj, property, propertyValue);
                }
                else if (propertyValue is IEnumerable)
                {
                    WriteEnumerableValue(writer, obj, property, propertyValue);
                }
                else
                {
                    WriteComplexValue(writer, obj, property, propertyValue);
                }
            }
        }

        private static void HandleWriteTextElement(System.Xml.XmlWriter writer, object obj)
        {
            var properties = GetPropertiesAttributeMap(obj.GetType());

            //  Handle all properties that serialized as text
            var textProperty = (from prop in properties
                                where prop.IsText
                                select prop).SingleOrDefault();

            if (textProperty != null)
            {
                var propertyValue = textProperty.Property.GetValue(obj, null);
                if (propertyValue != null)
                {
                    WriteValue(writer, propertyValue);
                }
            }
        }

        private static void WriteEnumerableValue(XmlWriter writer, object obj, PropertySerializationInfo property, object propertyValue)
        {
            bool openElement = false;

            if (property.ArrayRootAttribute != null)
            {
                writer.WriteStartElement(property.ArrayRootAttribute.Name.Name, property.ArrayRootAttribute.Name.Namespace);

                openElement = true;
            }
            else if (property.Property.PropertyType == typeof(object))
            {
                //  Handle situation when property of type object has array of XmlNode items
                //var attribute = (from attr in property.Attributes
                //                 where
                //                     attr.Type == typeof(object)
                //                 select attr).SingleOrDefault();
                var propertiesToAttributesMap = GetPropertiesAttributeMap(obj.GetType());

                var attribute = (from attr in
                                     (from a in propertiesToAttributesMap
                                      where a.Property == property.Property
                                      select a).OfType<PropertyAttributeSerializationInfo>()
                                 where
                                     attr.Type == typeof(object)
                                 select attr).SingleOrDefault();
                if (attribute == null)
                {
                    throw new InvalidOperationException("Property of type object cannot be serialized.");
                }
                //  TODO:   Check if need to create prefix here
                writer.WriteStartElement(attribute.Name.Name, attribute.Name.Namespace);

                openElement = true;
            }

            foreach (var value in propertyValue as IEnumerable)
            {
                if (value == null)
                {
                    //  TODO:   Do something when type is nullable then it should be serialized with nil="true" attribute node
                    continue;
                }

                //  Get property attribute for each induvidual element of the collection
                var valueAttribute = GetPropertyAttribute(obj, property, value.GetType());

                WriteElementValue(writer, obj, valueAttribute, value);
            }

            if (openElement)
            {
                writer.WriteEndElement();
            }

        }

        private static void WriteComplexValue(XmlWriter writer, object obj, PropertySerializationInfo property, object propertyValue)
        {
            var propertyAttribute = GetPropertyAttribute(obj, property, propertyValue.GetType());

            if (propertyAttribute != null)
            {
                WriteElementValue(writer, obj, propertyAttribute, propertyValue);
            }
            else
            {
                throw new NotImplementedException("propertyAttribute is not found.");
            }
        }

        private static void WriteElementValue(XmlWriter writer, object obj, PropertyAttributeSerializationInfo propertyAttribute, object propertyValue)
        {
            PropertyInfo property = propertyAttribute.Property;

            //
            //  TODO:   Determine if property value is null
            bool isNullable = false;


            if (propertyValue.GetType().IsPrimitive
                || propertyValue is string
                || propertyValue is System.Xml.XmlQualifiedName
                || propertyValue is Enum
                )
            {
                writer.WriteStartElement(propertyAttribute.Name.Name, propertyAttribute.Name.Namespace);

                WriteValue(writer, propertyValue);

                writer.WriteEndElement();
            }
            else if (propertyAttribute.IsNullable && isNullable)
            {
                //  TODO:   Write nullable value type
            }
            else if (propertyValue is XmlNode)
            {
                //  TODO:   XmlNode is IEnumrable, consider utilize this interface
                XmlNode node = propertyValue as XmlNode;

                if (node.NodeType == XmlNodeType.Attribute && node.NamespaceURI == "http://www.w3.org/2000/xmlns/" && node.LocalName == writer.LookupPrefix(node.Value))
                {
                    //  If node is an attribute node and declares namespace prefix which is already declared then igonre this value
                }
                else if (node.NodeType == XmlNodeType.Element)
                {
                    RemoveNodeNamespaceDeclaration(node, writer);
                    node.WriteTo(writer);
                }
                else if (node.NodeType == XmlNodeType.Attribute)
                {
                    node.WriteTo(writer);
                }
                else if (node.NodeType == XmlNodeType.Text)
                {
                    node.WriteTo(writer);
                }
                //  TODO:   Do something when XmlNode represents an attribute
                //else
                //{
                //    node.WriteTo(writer);
                //}
            }
            else if (propertyAttribute.Type == typeof(object))
            {
                //  TODO:   Write object value check first if its XmlNode[] and if not then default to object serialization
                //  TODO:   Check if prefix is required based on elementAttribute.Form value
                writer.WriteStartElement(propertyAttribute.Name.Name, propertyAttribute.Name.Namespace);

                var typeAttribute = (from attr in TypeAttributes
                                     where
                                     attr.Type == propertyValue.GetType()
                                     select attr).SingleOrDefault();

                if (typeAttribute != null)
                {
                    writer.WriteAttributeString("xsi", "type", null, string.Format("{0}:{1}", writer.LookupPrefix(typeAttribute.Attribute.Namespace), typeAttribute.Attribute.TypeName));
                    InteropSerializer.WriteXml(writer, propertyValue);
                }
                else
                {
                    writer.WriteAttributeString("xsi", "type", null, propertyValue.GetType().FullName);
                    InteropSerializer.WriteXml(writer, propertyValue);
                }

                writer.WriteEndElement();
            }
            //else if (property.PropertyType == typeof(System.Xml.XmlQualifiedName))
            //{
            //    var qn = propertyValue as System.Xml.XmlQualifiedName;
            //    writer.WriteStartElement(propertyAttribute.Name.Name, propertyAttribute.Name.Namespace);
            //    writer.WriteValue(qn);
            //    writer.WriteEndElement();
            //    //writer.WriteElementString(propertyAttribute.Name.Name, propertyAttribute.Name.Namespace, string.Format("{0}:{1}", writer.LookupPrefix(qn.Namespace), qn.Name));
            //}
            else if (typeof(System.Xml.XmlElement) == property.PropertyType)
            {
                //  TODO:   Write XmlElement value
            }
            //else if (property.PropertyType.GetInterface("IEnumerable") != null)
            else if (propertyValue is IEnumerable)
            {
                foreach (var item in propertyValue as IEnumerable)
                {
                    Debug.WriteLine(item);
                }
                throw new InvalidOperationException("Should never handle IEnumerable");
            }
            else
            {
                //  TODO:   Check if prefix is required based on elementAttribute.Form value
                writer.WriteStartElement(propertyAttribute.Name.Name, propertyAttribute.Name.Namespace);
                InteropSerializer.WriteXml(writer, propertyValue);
                writer.WriteEndElement();
            }
        }

        private static void WriteValue(XmlWriter writer, object propertyValue)
        {
            if (propertyValue.GetType().IsPrimitive
                || propertyValue is string
                || propertyValue is System.Xml.XmlQualifiedName)
            {
                writer.WriteValue(propertyValue);
            }
            else if (propertyValue is Enum)
            {
                var value = propertyValue.ToString();

                var field = propertyValue.GetType().GetField(propertyValue.ToString());

                var enumAttribute = field.GetCustomAttributes(true).OfType<System.Xml.Serialization.XmlEnumAttribute>().SingleOrDefault();
                if (enumAttribute != null)
                {
                    value = enumAttribute.Name;
                }

                writer.WriteValue(value);
            }
            else if (propertyValue is ICollection)
            {
                var total = 0;
                foreach (var itemValue in propertyValue as ICollection)
                {
                    WriteValue(writer, itemValue);
                    if (total++ > 1)
                    {
                        throw new NotSupportedException("Collection with multiple values are not supported.");
                    }
                }
            }
            else
            {
                throw new InvalidOperationException(string.Format("Cannot handle type {0}", propertyValue.GetType().FullName));
            }
        }

        private static PropertyAttributeSerializationInfo GetPropertyAttribute(object obj, PropertySerializationInfo property, Type propertyType)
        {
            var propertiesToAttributesMap = GetPropertiesAttributeMap(obj.GetType());

            var attributeValue = property.GetChoicePropertyValue(obj);

            PropertyAttributeSerializationInfo propertyAttribute = null;
            //  Get attribute using value specified by XmlChoiceIdentifierAttribute attribute
            if (attributeValue != null)
            {
                //propertyAttribute = (from attr in property.Attributes
                //                     where
                //                       attr.IsElement
                //                       && attributeValue != null
                //                       && attributeValue.Equals(attr.Name)
                //                     select attr).SingleOrDefault();
                propertyAttribute = (from attr in
                                         (from a in propertiesToAttributesMap
                                          where a.Property == property.Property
                                          select a).OfType<PropertyAttributeSerializationInfo>()
                                     where
                                       attr.IsElement
                                       && attributeValue != null
                                       && attributeValue.Equals(attr.Name)
                                     select attr).SingleOrDefault();
            }

            if (propertyAttribute == null)
            {
                //  Get type specific attribute
                //propertyAttribute = (from attr in property.Attributes
                //                     where
                //                       attr.IsElement
                //                       && attr.Type == propertyType
                //                     select attr).SingleOrDefault();
                propertyAttribute = (from attr in
                                         (from a in propertiesToAttributesMap
                                          where a.Property == property.Property
                                          select a).OfType<PropertyAttributeSerializationInfo>()
                                     where
                                       attr.IsElement
                                       && attr.Type == propertyType
                                     select attr).SingleOrDefault();
            }

            if (propertyAttribute == null)
            {
                //  Get attribute for Nullable types
                //propertyAttribute = (from attr in property.Attributes
                //                     where
                //                       attr.IsElement
                //                       && GetElementType(attr.Type) == propertyType
                //                     select attr).SingleOrDefault();
                propertyAttribute = (from attr in
                                         (from a in propertiesToAttributesMap
                                          where a.Property == property.Property
                                          select a).OfType<PropertyAttributeSerializationInfo>()
                                     where
                                       attr.IsElement
                                       && GetElementType(attr.Type) == propertyType
                                     select attr).SingleOrDefault();
            }

            if (propertyAttribute == null)
            {
                //  Get generic type attribute
                //propertyAttribute = (from attr in property.Attributes
                //                     where
                //                       attr.IsElement
                //                       && attr.Type == typeof(object)
                //                     select attr).SingleOrDefault();
                propertyAttribute = (from attr in
                                         (from a in propertiesToAttributesMap
                                          where a.Property == property.Property
                                          select a).OfType<PropertyAttributeSerializationInfo>()
                                     where
                                       attr.IsElement
                                       && attr.Type == typeof(object)
                                     select attr).SingleOrDefault();
            }

            if (propertyAttribute == null)
            {
                throw new NotImplementedException("Default PropertyAttributeSerializationInfo is not implemented.");
            }

            return propertyAttribute;
        }

        /// <summary>
        /// Removes the node namespace declaration if its already declared by the writer and has the same prefix
        /// </summary>
        /// <param name="node">The node.</param>
        /// <param name="writer">The writer.</param>
        private static void RemoveNodeNamespaceDeclaration(XmlNode node, XmlWriter writer)
        {
            var attributes = (from n in node.Attributes.OfType<XmlAttribute>()
                              where
                                 n.NamespaceURI == "http://www.w3.org/2000/xmlns/"
                                 && n.LocalName == writer.LookupPrefix(n.Value)
                              select n).ToList();

            foreach (var attribute in attributes)
            {
                node.Attributes.Remove(attribute);
            }

            foreach (XmlNode childNode in node.ChildNodes)
            {
                if (childNode.NodeType == XmlNodeType.Element)
                    RemoveNodeNamespaceDeclaration(childNode, writer);
            }
        }
    }
}
